package com.pason.onlineshop.mainshop.functionaltest.framework;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;

import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.google.common.base.Predicate;

public class Browser {

    public static final int DEFAULT_WAIT_TIMEOUT_SECONDS = 10;
    public static final int MAX_WAIT_TIMEOUT_SECONDS = DEFAULT_WAIT_TIMEOUT_SECONDS * 2;
    public static final int ORDER_CONFIRMATION_TIMEOUT_SECONDS = 60;
    public static final int AJAX_MAX_WAIT_TIMEOUT_MILLI_SECONDS = 90000;
    public static final int MAX_WAIT_TIMEOUT_MILLI_SECONDS = 30000;
    public static final int NOT_VISIBLE_WAIT_TIMEOUT_SECONDS = 3;
    protected static final Logger logger = LoggerFactory.getLogger(Browser.class);
    private static final String HTMLUNIT = "HTMLUNIT";
    private WebDriver driver;
    private int serverPort;
    private String webDriverType;
    private String host;

    Browser(final WebDriver driver, final String webDriverType, final String host, final int serverPort) {
        this.host = host;
        this.driver = driver;
        this.webDriverType = webDriverType;
        this.serverPort = serverPort;

        logger.debug("Creating browser: " + host + ":" + serverPort);
        logger.debug("WebDriver type: " + webDriverType);
    }

    public void ajaxClick(final String target) {
        try {
            ajaxClick(target, AJAX_MAX_WAIT_TIMEOUT_MILLI_SECONDS);
            turnOffJQueryAnimations();
        } catch (TimeoutException te) {
            // throw new RuntimeException(
            // "Timed out waiting for a jQuery AJAX call to return.  Are you sure the button triggers an Ajax request?",
            // te);
        }
    }

    /**
     * Use this method to click a button if you expect it to trigger an ajax call. This method will
     * block until the AJAX call completes. It will throw an exception if it times out after 10
     * seconds.
     *
     * @param target
     */
    public void ajaxClick(final String target, final long timeout) {

        /**
         * setup the callback
         */
        JavascriptExecutor executor = getJavaScriptExecutor();
        String clickButton = "$(arguments[0]).click();";
        String script = "var callback = arguments[arguments.length - 1]; " + "$(document).ajaxStop(function(){" + "callback();" + "});"
                + clickButton;

        driver.manage().timeouts().setScriptTimeout(timeout, TimeUnit.MILLISECONDS);
        executor.executeAsyncScript(script, waitTillElementPresent(By.id(target)));
    }

    /**
     * Same as ajaxClick, but will return true if it timed out, or false otherwise.
     *
     * @param target
     */
    public boolean ajaxClickTimedOut(final String target) {
        try {
            ajaxClick(target);
            return false;
        } catch (Exception e) {
            return true;
        }
    }

    public void clear(final String target) {
        type(target, "");
    }

    public void click(final By target) {
        WebElement elem = waitTillElementClickable(target);
        try {
            elem.click();
        } catch (WebDriverException e) {
            if (e.getMessage().contains("Element is not clickable")) {
                jQueryClick(waitTillElementClickable(target));
            } else {
                throw e;
            }
        }
    }

    public void click(final String target) {
        click(By.id(target));
    }

    /**
     * Note: some tests will need the method below to click properly.
     *
     * @param target
     */
    public void clickWhenEventIsBound(final By target) {
        waitTillEventIsBound(target, "click").click();
    }

    public String escapeJQueryId(final String target) {
        String jQueryTarget = target.replace(".", "\\\\.");
        return jQueryTarget;
    }

    public void executeAsyncScript(final String script, final Object... args) {
        getJavaScriptExecutor().executeAsyncScript(script, args);
    }

    public Object executeScript(final String script, final Object... args) {
        return getJavaScriptExecutor().executeScript(script, args);
    }

    public WebElement findAndWaitVisible(final By by) {
        return waitTillElementVisible(by);
    }

    public WebElement findElement(final By selector) {
        return driver.findElement(selector);
    }

    public WebElement findElement(final String id) {
        return driver.findElement(By.id(id));
    }

    public List<WebElement> findElements(final By selector) {
        return driver.findElements(selector);
    }

    public void focusBody() {
        waitForJQueryToBeDefined();
        ((JavascriptExecutor) driver).executeScript("$('body').focus();");
    }

    public void get(final String url) {
        String fullUrl = "http://" + host + ":" + serverPort + url;
        logger.debug("get(" + fullUrl + ")");
        driver.get(fullUrl);
    }


    public String getCurrentUrl() {
        return driver.getCurrentUrl();
    }

    public WebDriver getDriver() {
        return driver;
    }

    public String getHiddenValue(final String target) {
        return (String) executeScript("return $('#" + target + "').val();");
    }

    public WebElement getIdWithDelay(final String targetId) {
        return findAndWaitVisible(By.id(targetId));
    }

    public WebElement getIdWithDelay(final String targetId, final long millisecondsToWait, final boolean isFailOnNotFound) {
        WebElement element = null;
        List<WebElement> elements = driver.findElements(By.id(targetId));
        if (!elements.isEmpty()) {
            element = elements.get(0);
        } else {
            if (isFailOnNotFound) {
                assertTrue("Element with id " + targetId + " not found in the DOM" + getTrace(), false);
            } else {
                element = null;
            }
        }
        return element;

    }

    // eg the value of an input field
    public String getInputValue(final String target) {
        return getIdWithDelay(target).getAttribute("value");
    }

    public JavascriptExecutor getJavaScriptExecutor() {
        return (JavascriptExecutor) driver;
    }

    public Select getSelect(final String target) {
        WebElement webElement = getIdWithDelay(target);
        return new Select(webElement);
    }

    public int getServerPort() {
        return serverPort;
    }

    // eg text in a div or span
    public String getText(final String target) {
        return waitTillElementPresent(By.id(target)).getText();
    }

    public String getTrace() {
        return " \n";
    }

    public boolean isExists(final By target) {
        boolean found;
        try {
            driver.manage().timeouts().implicitlyWait(Browser.DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            driver.findElement(target);
            found = true;
        } catch (NoSuchElementException e) {
            found = false;
        }
        return found;
    }

    public boolean isNotExists(final By target) {
        boolean found;
        try {
            driver.manage().timeouts().implicitlyWait(Browser.NOT_VISIBLE_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            driver.findElement(target);
            found = true;
        } catch (NoSuchElementException e) {
            found = false;
        }
        driver.manage().timeouts().implicitlyWait(Browser.DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        return !found;
    }

    public boolean isHtmlUnit() {
        return getWebDriverType().equalsIgnoreCase(HTMLUNIT);
    }

    public Boolean isJqueryVisible(final WebElement target) {
        // Google Search - Nov 29, 2010 - Another bug in WebDriver or HtmlUnit: isDisplayed()
        // doesn't work for css applied through classes, although it works for style=""

        String script = "return $(arguments[0]).is(':visible');";
        JavascriptExecutor js = (JavascriptExecutor) driver;
        return (Boolean) js.executeScript(script, target);
    }

    public Boolean isNotVisible(final String target) {
        return isNotVisible(target, DEFAULT_WAIT_TIMEOUT_SECONDS);
    }

    public boolean isNotVisible(final String target, final long millisecondsToWait) {

        return this.isNotVisible(By.id(target), millisecondsToWait);

    }

    public boolean isNotVisible(final By target, final long millisecondsToWait) {

        try {
            return new WebDriverWait(driver, millisecondsToWait / 1000).until(notVisible(target));
        } catch (Exception e) {
            return false;
        }

    }

    public boolean isVisible(final String target) {
        return driver.findElement(By.id(target)).isDisplayed();
    }

    public void jsonpClick(final By selector) {
        waitForJQueryToBeDefined();
        executeAsyncScript("if ($ && $.jsonp) {" + "$.jsonp.setup({" + "complete : arguments[ arguments.length - 1 ]" + "});" + "}"
                + "$(arguments[0]).click();", waitTillElementPresent(selector));
    }

    public void mouseOver(final By target) {
        WebElement targetElem = waitTillElementClickable(target);
        Actions builder = new Actions(getDriver());
        Actions hoverOverRegistrar = builder.moveToElement(targetElem);
        hoverOverRegistrar.perform();
    }

    public void reset() {
        getDriver().manage().deleteAllCookies();
    }

    public void scrollDown() {
        executeScript("(0,250);");
    }

    // it is also possible to scroll up by changing x,y coordinate:
    public void scrollUp() {
        waitForJQueryToBeDefined();
        executeScript("window.scrollTo(0,0);");
    }

    public void selectByText(final By target, final String text) {
        Select select = new Select(waitTillElementVisible(target));
        select.selectByVisibleText(text);
        assertEquals(text, select.getFirstSelectedOption().getText());
        focusBody();
    }

    public void selectByValue(final By target, final String value) {
        Select select = new Select(waitTillElementVisible(target));
        select.selectByValue(value);
        assertEquals(value, select.getFirstSelectedOption().getAttribute("value"));
        focusBody();
    }

    public void sleep(final long millisecondsToWait) {
        if (millisecondsToWait > 0) {
            try {
                Thread.sleep(millisecondsToWait);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public String text(final By selector) {
        return findElement(selector).getText();
    }

    public void turnOffJQueryAnimations() {
        waitForJQueryToBeDefined();
        executeScript("$.fx.off = true;");
    }

    /**
     * This will simply ensure the input field contains the value passed. No Key events are
     * triggered. If you need key events to trigger see
     *
     * @param target
     * @param value
     * @throws InterruptedException public void type(final String target, final String value) { executeScript("$('#"
     *                              + escapeJQueryId(target) + "').val('" + value + "')");
     *                              waitUntilInputValueIs(target, value); }
     */

    public void type(final String target, final String value) {
        WebElement elem = waitTillElementPresent(By.id(target));
        jQuerySetVal(elem, value);
        /*
         * if ("".equals(value)) { jQuerySetVal(elem, ""); } else { // send all but the last key so
         * THAT WE TRIGGER AN ACTUAL EVENT. String allButLast = value.substring(0, value.length() -
         * 1); jQuerySetVal(elem, allButLast); String lastChar = "" + value.charAt(value.length() -
         * 1); elem.sendKeys(lastChar); }
         */
        waitUntilInputValueIs(target, value);
    }

    public String value(final By selector) {
        return findElement(selector).getAttribute("value");
    }

    public void waitForElement(final By selector) {
        new WebDriverWait(driver, defaultWaitInSeconds()).until(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(final WebDriver arg0) {
                try {
                    arg0.findElement(selector);
                    return true;
                } catch (ElementNotFoundException e) {
                    return false;
                }
            }
        });
    }

    public void waitForJQueryToBeDefined() {
        new WebDriverWait(getDriver(), DEFAULT_WAIT_TIMEOUT_SECONDS).until(new Predicate<WebDriver>() {
            @Override
            public boolean apply(final WebDriver arg0) {
                return Boolean.valueOf(executeScript("return typeof $ !== 'undefined'").toString());
            }
        });
    }

    public WebElement waitTillElementPresent(final By target) {
        return waitTillElementPresent(target, DEFAULT_WAIT_TIMEOUT_SECONDS);
    }

    public WebElement waitTillElementPresent(final By target, final long secondsToWait) {
        try {
            // Published defect where implicitWait not impacted by local change.
            driver.manage().timeouts().implicitlyWait(secondsToWait, TimeUnit.SECONDS);
            return new WebDriverWait(driver, secondsToWait, 100).until(ExpectedConditions.presenceOfElementLocated(target));
        } finally {
            driver.manage().timeouts().implicitlyWait(Browser.DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        }
    }

    public WebElement waitTillElementVisible(final By target, final long waitSeconds) {
        return findAndWaitVisible(target, waitSeconds > 0 ? waitSeconds : DEFAULT_WAIT_TIMEOUT_SECONDS);
    }

    public WebElement waitTillElementVisible(final By target) {
        return findAndWaitVisible(target, DEFAULT_WAIT_TIMEOUT_SECONDS);
    }

    public WebElement waitTillEventIsBound(final By target, final String eventName) {
        final String waitForEvent = "try {" + "return typeof ($(arguments[0]).data('events')." + eventName + ") === 'object';"
                + "} catch(e) {" + "return false;" + "}";

        new WebDriverWait(driver, defaultWaitInSeconds()).until(new ExpectedCondition<Object>() {
            @Override
            public Object apply(@Nullable final WebDriver webDriver) {
                return executeScript(waitForEvent, webDriver.findElement(target));
            }
        });

        return driver.findElement(target);
    }

    public boolean waitTillVisible(final By by) {
        return waitTillVisible(by, 0);
    }

    public boolean waitTillVisible(final By by, final long waitSeconds) {
        try {
            waitTillElementVisible(by, waitSeconds);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public boolean waitTillVisible(final String target) {
        return waitTillVisible(By.id(target), 0);
    }

    public boolean waitTillVisible(final String target, final long waitSeconds) {
        return waitTillVisible(By.id(target), waitSeconds);
    }

    private long defaultWaitInSeconds() {
        return 5000 / 1000;
    }

    private void jQueryClick(final WebElement waitTillElementClickable) {
        executeScript("$(arguments[0]).trigger('click')", waitTillElementClickable);
    }

    private void jQuerySetVal(final WebElement target, final String allButLast) {
        waitForJQueryToBeDefined();
        executeScript("$(arguments[0]).val('" + allButLast + "')", target);
    }

    @SuppressWarnings("unused")
    private String normalizeElementId(final String elementId) {
        return elementId.replace(".", "\\\\.");
    }

    private ExpectedCondition<Boolean> notVisible(final String target) {
        return notVisible(By.id(target));
    }

    private ExpectedCondition<Boolean> notVisible(final By target) {
        ExpectedCondition<Boolean> realBrowserPredicate = new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(@Nullable final WebDriver d) {
                // element is not present
                List<WebElement> elements = d.findElements(target);
                if (CollectionUtils.isEmpty(elements)) {
                    return true;
                }

                return (!isJqueryVisible(elements.get(0)));
            }
        };

        ExpectedCondition<Boolean> htmlUnitPredicate = new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(@Nullable final WebDriver d) {
                // element is not present
                List<WebElement> elements = d.findElements(target);
                if (CollectionUtils.isEmpty(elements)) {
                    return true;
                }

                if (!isJqueryVisible(elements.get(0))) {
                    return true;
                }

                return ("".equals(elements.get(0).getText()));
            }
        };

        return (isHtmlUnit()) ? htmlUnitPredicate : realBrowserPredicate;
    }

    private WebElement waitTillElementClickable(final By target) {
        WebElement element = waitTillElementVisible(target);
        if (!StringUtils.equalsIgnoreCase(element.getTagName(), "button")) {
            return element; // only care for button
        }
        if ("input".equalsIgnoreCase(element.getTagName()) && "submit".equalsIgnoreCase(element.getAttribute("type"))) {
            return element; // submit button do not have event handler
        }
        if (StringUtils.isNotBlank(element.getAttribute("onclick"))) {
            return element; // inline event handler
        }
        if (StringUtils.containsIgnoreCase(element.getAttribute("class"), "tms-lb-close")) {
            return element; // close icon
        }
        return element;
    }

    private void waitUntilInputValueIs(final String target, final String value) {
        new WebDriverWait(driver, DEFAULT_WAIT_TIMEOUT_SECONDS).until(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(final WebDriver arg0) {
                WebElement elem = arg0.findElement(By.id(target));
                String elemValue = elem.getAttribute("value");
                return elemValue.equals(value);
            }
        });
    }

    protected WebElement findAndWaitVisible(final By by, final long waitSeconds) {
        driver.manage().timeouts().implicitlyWait(waitSeconds, TimeUnit.SECONDS);
        WebElement element = new WebDriverWait(driver, waitSeconds).until(new ExpectedCondition<WebElement>() {
            @Override
            public WebElement apply(@Nullable final WebDriver d) {
                try {
                    WebElement el = d.findElement(by);
                    if (isHiddenInput(el)) {
                        return el;
                    } else {
                        return (isJqueryVisible(el)) ? el : null;
                    }
                } catch (NoSuchElementException e) {
                    return null;
                }
            }
        });
        driver.manage().timeouts().implicitlyWait(Browser.DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        return element;
    }

    private boolean isHiddenInput(WebElement el) {
        return "input".equalsIgnoreCase(el.getTagName()) && "hidden".equalsIgnoreCase(el.getAttribute("type"));
    }

    protected void waitUntilSelectTextIs(final By by, final String value) {
        new WebDriverWait(driver, DEFAULT_WAIT_TIMEOUT_SECONDS).until(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(final WebDriver arg0) {
                WebElement option = new Select(waitTillElementPresent(by)).getFirstSelectedOption();
                String text = option.getText();
                return value.equals(text);
            }
        });
    }

    protected void waitUntilSelectValueIs(final By by, final String value) {
        new WebDriverWait(driver, DEFAULT_WAIT_TIMEOUT_SECONDS).until(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(final WebDriver arg0) {
                WebElement option = new Select(waitTillElementPresent(by)).getFirstSelectedOption();
                String optionValue = option.getAttribute("value");
                return value.equals(optionValue);
            }
        });
    }

    String getWebDriverType() {
        return webDriverType;
    }

    public boolean isUrl(final String url) {
        String currentUrl = getCurrentUrl();
        for (int i = 0; i < 70; i++ ) {
            if ((currentUrl != null) && ((currentUrl.endsWith(url) || currentUrl.endsWith(url + "/")))) {
                return true;
            }
            sleep(100);
            currentUrl = getCurrentUrl();
        }
        return false;
    }

}